<?php

namespace Illuminate\Events;

use Exception;
use Illuminate\Container\Container;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastFactory;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Container\Container as ContainerContract;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use ReflectionClass;

/**
 * Laravel 事件调度器程序
 */
class Dispatcher implements DispatcherContract
{
    /**
     * Laravel IoC 容器实例
     *
     * @var \Illuminate\Contracts\Container\Container
     */
    protected $container;

    /**
     * 已注册的事件监听器（每个元素都是一个 Closure 对象）
     *
     * 格式：['event' => [Closure, ...]]
     *
     * @var array
     */
    protected $listeners = [];

    /**
     * 已注册的通配符事件监听器（每个元素都是一个 Closure 对象）
     *
     * 格式：['event' => [Closure, ...]]
     *
     * @var array
     */
    protected $wildcards = [];

    /**
     * 通配符事件监听器的缓存（但是，每注册一个新的通配符监听器，都会清空缓存）
     *
     * 格式：['event' => [Closure, ...]]
     *
     * @var array
     */
    protected $wildcardsCache = [];

    /**
     * 队列解析器实例
     *
     * @var callable
     */
    protected $queueResolver;

    /**
     * 创建一个新的事件调度器实例
     *
     * @param  \Illuminate\Contracts\Container\Container|null $container
     * @return void
     */
    public function __construct(ContainerContract $container = null)
    {
        $this->container = $container ?: new Container;
    }

    /**
     * 注册一个事件监听器
     *
     * @param  string|array $events 事件名称（支持通配符、支持数组）
     *                              支持数组: ['boot','start']
     *                              支持通配符: boot.*
     *
     * @param  mixed $listener 监听器
     *                         支持闭包函数
     *                         支持字符串格式: Class@handle
     * @return void
     */
    public function listen($events, $listener)
    {
        foreach ((array)$events as $event) {
            if (Str::contains($event, '*')) {
                $this->setupWildcardListen($event, $listener);
            } else {
                $this->listeners[$event][] = $this->makeListener($listener);
            }
        }
    }

    /**
     * 安装一个通配符的监听器
     *
     * @param  string $event 通配符事件名
     * @param  mixed $listener 监听器
     * @return void
     */
    protected function setupWildcardListen($event, $listener)
    {
        $this->wildcards[$event][] = $this->makeListener($listener, true);

        $this->wildcardsCache = [];
    }

    /**
     * 确认给定的事件是否注册了监听器（支持通配符）
     *
     * @param  string $eventName 事件名
     * @return bool
     */
    public function hasListeners($eventName)
    {
        return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);
    }

    /**
     * 推入一个延迟触发事件，并在使用 flush 调用该事件的时候使用指定的参数
     *
     * @param  string $event 延迟触发的事件名
     * @param  array $payload 指定参数
     * @return void
     */
    public function push($event, $payload = [])
    {
        $this->listen($event . '_pushed', function () use ($event, $payload) {
            $this->dispatch($event, $payload);
        });
    }

    /**
     * 触发一组延迟调用的事件
     *
     * @param  string $event 推送事件名
     * @return void
     */
    public function flush($event)
    {
        $this->dispatch($event . '_pushed');
    }

    /**
     * 注册一个事件订阅者（其实就是直接调用订阅者的 subscribe 方法，并传递当前对象作为参数）
     *
     * @param  object|string $subscriber 订阅者对象 | 订阅者类名
     * @return void
     */
    public function subscribe($subscriber)
    {
        $subscriber = $this->resolveSubscriber($subscriber);

        $subscriber->subscribe($this);
    }

    /**
     * 解析出一个订阅者实例
     *
     * @param  object|string $subscriber
     * @return mixed
     */
    protected function resolveSubscriber($subscriber)
    {
        if (is_string($subscriber)) {
            return $this->container->make($subscriber);
        }

        return $subscriber;
    }

    /**
     * 触发一个事件，并得到第一个非空响应
     *
     * @param  string|object $event 事件名 | 事件对象
     * @param  mixed $payload 传递参数
     * @return array|null
     */
    public function until($event, $payload = [])
    {
        return $this->dispatch($event, $payload, true);
    }

    /**
     * 触发指定事件，并调用事件监听器
     *
     * @param  string|object $event 事件名 | 事件对象
     * @param  mixed $payload 监听器参数
     * @param  bool $halt 是否在接收到一个不为 null 的数据时，立即返回该数据
     * @return array|null
     */
    public function dispatch($event, $payload = [], $halt = false)
    {

        // 当给定的“事件”实际上是一个对象时，我们将其假定为事件对象，并将该类用作事件名称，
        // 将此事件对象本身作为监听器的参数，这使得基于对象的事件监听器非常简单。
        [$event, $payload] = $this->parseEventAndPayload(
            $event, $payload
        );

        // 如果是需要广播的事件，就发起广播
        if ($this->shouldBroadcast($payload)) {
            $this->broadcastEvent($payload[0]);
        }

        $responses = [];

        foreach ($this->getListeners($event) as $listener) {
            $response = $listener($event, $payload);

            // 如果监听器返回了响应数据，并且启用了事件暂停，我们直接原样返回响应数据。
            // 并且不再调用其余的事件监听器，否则我们将会把所有监听器的的响应放入列表中，统一返回。
            if ($halt && !is_null($response)) {
                return $response;
            }

            // 如果任何一个监听器明确的返回了一个 false 值，我们将立即停止继续调用其他的监听器，
            // 并直把所有已执行成功的监听器的返回值，放到一个数组中直接返回
            if ($response === false) {
                break;
            }

            $responses[] = $response;
        }


        return $halt ? null : $responses;
    }

    /**
     * 解析给定的事件，并把参数包装成数组，为分发事件做好准备
     *
     * @param  mixed $event 事件名 | 事件对象
     * @param  mixed $payload 参数
     * @return array [事件名，参数数组]
     */
    protected function parseEventAndPayload($event, $payload)
    {
        // 如果事件是个对象类型，就把类名作为事件名，事件本身作为参数
        if (is_object($event)) {
            [$event, $payload] = [get_class($event), [$event]];
        }

        return [$event, Arr::wrap($payload)];
    }

    /**
     * Determine if the payload has a broadcastable event.
     *
     * @param  array $payload
     * @return bool
     */
    protected function shouldBroadcast(array $payload)
    {
        return isset($payload[0]) &&
            $payload[0] instanceof ShouldBroadcast &&
            $this->broadcastWhen($payload[0]);
    }

    /**
     * Check if event should be broadcasted by condition.
     *
     * @param  mixed $event
     * @return bool
     */
    protected function broadcastWhen($event)
    {
        return method_exists($event, 'broadcastWhen')
            ? $event->broadcastWhen() : true;
    }

    /**
     * Broadcast the given event class.
     *
     * @param  \Illuminate\Contracts\Broadcasting\ShouldBroadcast $event
     * @return void
     */
    protected function broadcastEvent($event)
    {
        $this->container->make(BroadcastFactory::class)->queue($event);
    }

    /**
     * 获取给定事件名的所有监听器闭包（包括通配符监听器）
     *
     * @param  string $eventName 事件名
     * @return array
     */
    public function getListeners($eventName)
    {
        // 获取标准方式注册的监听器列表
        $listeners = $this->listeners[$eventName] ?? [];

        // 然后再拼接上通配符监听器
        $listeners = array_merge(
            $listeners,
            $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
        );

        // 如果事件名是个类名，就把它所实现的所有接口的监听器也并入其中，而普通事件名则直接返回监听器即可
        return class_exists($eventName, false)
            ? $this->addInterfaceListeners($eventName, $listeners)
            : $listeners;
    }

    /**
     * 指定事件名，获取所有能够匹配上的通配符监听器闭包，并且把监听器数组存入缓存
     *
     * @param  string $eventName 事件名
     * @return array
     */
    protected function getWildcardListeners($eventName)
    {
        $wildcards = [];

        foreach ($this->wildcards as $key => $listeners) {
            if (Str::is($key, $eventName)) {
                $wildcards = array_merge($wildcards, $listeners);
            }
        }

        return $this->wildcardsCache[$eventName] = $wildcards;
    }

    /**
     * 将指定事件类所实现的接口的所有监听器，也都追加到监听器列表中，并返回
     *
     * @param  string $eventName 事件类名
     * @param  array $listeners 监听器
     * @return array
     */
    protected function addInterfaceListeners($eventName, array $listeners = [])
    {
        foreach (class_implements($eventName) as $interface) {
            if (isset($this->listeners[$interface])) {
                foreach ($this->listeners[$interface] as $names) {
                    $listeners = array_merge($listeners, (array)$names);
                }
            }
        }

        return $listeners;
    }

    /**
     * 根据给定的监听器格式，创建一个可直接执行的监听器闭包对象
     *
     * @param  \Closure|string $listener 监听器
     *                                   可以是 Class@handle 格式
     *                                   也可以是一个 Closure 对象
     * @param  bool $wildcard 是否包含通配符
     * @return \Closure
     */
    public function makeListener($listener, $wildcard = false)
    {

        // 如果是类监听器，就使用 Ioc 容器创建对象，并封装成可直接调用的闭包
        if (is_string($listener)) {
            return $this->createClassListener($listener, $wildcard);
        }

        // 如果是通配符监听器，就把具体的事件名（对象）作为第一个参数
        return function ($event, $payload) use ($listener, $wildcard) {
            if ($wildcard) {
                return $listener($event, $payload);
            }

            return $listener(...array_values($payload));
        };
    }

    /**
     * 使用 IoC 容器创建基于类的对象，并封装为可直接执行的闭包函数
     *
     * @param  string $listener 类监听器
     * @param  bool $wildcard 是否包含通配符
     * @return \Closure
     */
    public function createClassListener($listener, $wildcard = false)
    {
        return function ($event, $payload) use ($listener, $wildcard) {
            if ($wildcard) {
                return call_user_func($this->createClassCallable($listener), $event, $payload);
            }

            return call_user_func_array(
                $this->createClassCallable($listener), $payload
            );
        };
    }

    /**
     * 创建类名格式的监听器闭包，并返回可供 call_user_func 调用的格式数组
     *
     * @param  string $listener 类监听器
     * @return callable|array
     */
    protected function createClassCallable($listener)
    {
        [$class, $method] = $this->parseClassCallable($listener);

        if ($this->handlerShouldBeQueued($class)) {
            return $this->createQueuedHandlerCallable($class, $method);
        }

        return [$this->container->make($class), $method];
    }

    /**
     * 将类监听器字符串，解析为 [类名,方法名] 的数组
     *
     * @param  string $listener
     * @return array
     */
    protected function parseClassCallable($listener)
    {
        return Str::parseCallback($listener, 'handle');
    }

    /**
     * 确定事件监听器是否放入队列（实现了 ShouldQueue 接口的类）
     *
     * @param  string $class 监听器类名
     * @return bool
     */
    protected function handlerShouldBeQueued($class)
    {
        try {
            return (new ReflectionClass($class))->implementsInterface(
                ShouldQueue::class
            );
        } catch (Exception $e) {
            return false;
        }
    }

    /**
     * Create a callable for putting an event handler on the queue.
     *
     * @param  string $class
     * @param  string $method
     * @return \Closure
     */
    protected function createQueuedHandlerCallable($class, $method)
    {
        return function () use ($class, $method) {
            $arguments = array_map(function ($a) {
                return is_object($a) ? clone $a : $a;
            }, func_get_args());

            if ($this->handlerWantsToBeQueued($class, $arguments)) {
                $this->queueHandler($class, $method, $arguments);
            }
        };
    }

    /**
     * Determine if the event handler wants to be queued.
     *
     * @param  string $class
     * @param  array $arguments
     * @return bool
     */
    protected function handlerWantsToBeQueued($class, $arguments)
    {
        $instance = $this->container->make($class);

        if (method_exists($instance, 'shouldQueue')) {
            return $instance->shouldQueue($arguments[0]);
        }

        return true;
    }

    /**
     * Queue the handler class.
     *
     * @param  string $class
     * @param  string $method
     * @param  array $arguments
     * @return void
     */
    protected function queueHandler($class, $method, $arguments)
    {
        [$listener, $job] = $this->createListenerAndJob($class, $method, $arguments);

        $connection = $this->resolveQueue()->connection(
            $listener->connection ?? null
        );

        $queue = $listener->queue ?? null;

        isset($listener->delay)
            ? $connection->laterOn($queue, $listener->delay, $job)
            : $connection->pushOn($queue, $job);
    }

    /**
     * Create the listener and job for a queued listener.
     *
     * @param  string $class
     * @param  string $method
     * @param  array $arguments
     * @return array
     */
    protected function createListenerAndJob($class, $method, $arguments)
    {
        $listener = (new ReflectionClass($class))->newInstanceWithoutConstructor();

        return [$listener, $this->propagateListenerOptions(
            $listener, new CallQueuedListener($class, $method, $arguments)
        )];
    }

    /**
     * Propagate listener options to the job.
     *
     * @param  mixed $listener
     * @param  mixed $job
     * @return mixed
     */
    protected function propagateListenerOptions($listener, $job)
    {
        return tap($job, function ($job) use ($listener) {
            $job->tries = $listener->tries ?? null;
            $job->retryAfter = $listener->retryAfter ?? null;
            $job->timeout = $listener->timeout ?? null;
            $job->timeoutAt = method_exists($listener, 'retryUntil')
                ? $listener->retryUntil() : null;
        });
    }

    /**
     * 从事件调度程序中删除一组侦听器
     *
     * @param  string $event 事件名
     * @return void
     */
    public function forget($event)
    {
        if (Str::contains($event, '*')) {
            unset($this->wildcards[$event]);
        } else {
            unset($this->listeners[$event]);
        }
    }

    /**
     * 移除所有的延迟触发事件
     *
     * @return void
     */
    public function forgetPushed()
    {
        foreach ($this->listeners as $key => $value) {
            if (Str::endsWith($key, '_pushed')) {
                $this->forget($key);
            }
        }
    }

    /**
     * 从依赖的解析器中获取队列程序实例
     *
     * @return \Illuminate\Contracts\Queue\Queue
     */
    protected function resolveQueue()
    {
        return call_user_func($this->queueResolver);
    }

    /**
     * 设置队列程序的解析器
     *
     * @param  callable $resolver 解析器，必须返回一个 Queue 接口的实例
     * @return $this
     */
    public function setQueueResolver(callable $resolver)
    {
        $this->queueResolver = $resolver;

        return $this;
    }
}
